<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    通过diff算法实现CRUD
    <script>
      // 初始化
      let vDom = void 0;
      // 创建虚拟DOM节点的函数
      function createElement(type, props, ...children) {
        return {
          type,
          props: props || {},
          children: children.flat(),
        };
      }

      // 将虚拟DOM转换为真实DOM的函数
      function render(vnode) {
        // 如果是文本节点,直接创建文本节点
        if (typeof vnode === "string" || typeof vnode === "number") {
          return document.createTextNode(vnode);
        }
        // 创建元素
        const element = document.createElement(vnode.type);
        // 设置属性
        for (let key in vnode.props) {
          element.setAttribute(key, vnode.props[key]);
        }
        // 递归渲染子节点
        vnode.children.forEach((child) => {
          element.appendChild(render(child));
        });
        return element;
      }

      // 递归搜索并更新节点
      function updateNode(vnode, key, updates) {
        if (vnode.props && vnode.props.key === key) {
          // 找到目标节点,应用更新
          Object.assign(vnode.props, updates.props || {});
          if (updates.children) {
            vnode.children = updates.children;
          }
          return;
        }
        // 递归搜索子节点
        vnode.children.forEach((child) => {
          if (typeof child === "object") {
            updateNode(child, key, updates);
          }
        });
      }

      // 创建一个函数来更新段落内容
      function updateParagraph() {
        // 根据key值更新段落内容
        // 复制一份vDom
        const newVDom = JSON.parse(JSON.stringify(vDom));
        // 在新的vDom中更新节点
        updateNode(newVDom, "paragraph", {
          props: { style: "color: red", key: "paragraph" },
          children: ["段落内容已更新!"],
        });
        // 获取根元素
        const container = document.querySelector("#root");
        // 使用diff算法对比新旧vDom并更新DOM
        diff(vDom, newVDom, container.parentElement);
        vDom = newVDom;
      }

      function diff(oldVNode, newVNode, parent) {
        // 1. 处理文本节点
        // 如果新旧节点中有任何一个是文本节点(字符串或数字)
        if (
          typeof oldVNode === "string" ||
          typeof oldVNode === "number" ||
          typeof newVNode === "string" ||
          typeof newVNode === "number"
        ) {
          // 如果文本内容不同，直接更新父节点的文本内容
          if (oldVNode !== newVNode) {
            parent.textContent = newVNode;
          }
          return;
        }

        // 2. 获取当前DOM节点
        // 通过key属性在父节点中查找对应的DOM元素
        const currentElement = parent.querySelector(
          `[key="${oldVNode.props.key}"]`
        );
        // 如果找不到对应元素则返回
        if (!currentElement) return;

        // 3. 比较并更新属性
        // 获取新旧节点的属性对象
        const oldProps = oldVNode.props || {};
        const newProps = newVNode.props || {};

        // 删除不再需要的旧属性
        Object.keys(oldProps).forEach((key) => {
          // 如果新属性中不存在该属性
          if (!(key in newProps)) {
            if (key === "style") {
              // 清空style属性
              currentElement.style = "";
            } else if (key.startsWith("on")) {
              // 移除事件监听器
              const eventName = key.slice(2).toLowerCase();
              currentElement[eventName] = null;
            } else {
              // 移除普通属性
              currentElement.removeAttribute(key);
            }
          }
        });

        // 设置或更新新属性
        Object.keys(newProps).forEach((key) => {
          // 如果属性值发生变化
          if (oldProps[key] !== newProps[key]) {
            if (key === "style") {
              // 更新style属性
              currentElement.style = newProps[key];
            } else if (key.startsWith("on")) {
              // 更新事件处理函数
              const eventName = key.slice(2).toLowerCase();
              currentElement[key] = new Function(newProps[key]);
            } else {
              // 更新普通属性
              currentElement.setAttribute(key, newProps[key]);
            }
          }
        });

        // 4. 比较并更新子节点
        // 获取新旧节点的子节点数组
        const oldChildren = oldVNode.children || [];
        const newChildren = newVNode.children || [];

        // 获取最大子节点数量
        const maxLength = Math.max(oldChildren.length, newChildren.length);

        // 递归比较每个子节点
        for (let i = 0; i < maxLength; i++) {
          const oldChild = oldChildren[i];
          const newChild = newChildren[i];

          // 如果新节点不存在但旧节点存在，则删除旧节点
          if (!newChild && oldChild) {
            const childElement = currentElement.querySelector(
              `[key="${oldChild.props?.key}"]`
            );
            if (childElement) {
              currentElement.removeChild(childElement);
            }
            continue;
          }

          // 如果旧节点不存在但新节点存在，则添加新节点
          if (!oldChild && newChild) {
            currentElement.appendChild(render(newChild));
            continue;
          }

          // 如果新旧节点都存在，递归进行diff比较
          diff(oldChild, newChild, currentElement);
        }
      }

      function addItem() {
        // 复制一份vDom
        const newVDom = JSON.parse(JSON.stringify(vDom));
        // 获取list节点
        const listNode = newVDom.children.find(child => child.props.key === 'list');
        // 生成新的key
        const newKey = `item-${listNode.children.length + 1}`;
        // 添加新的li节点
        listNode.children.push(
          createElement('li', { key: newKey }, `项目${listNode.children.length + 1}`)
        );
        // 获取根元素
        const container = document.querySelector("#root");
        // 使用diff算法对比新旧vDom并更新DOM
        diff(vDom, newVDom, container.parentElement);
        vDom = newVDom;
      }

      function deleteItem() {
        // 复制一份vDom
        const newVDom = JSON.parse(JSON.stringify(vDom));
        // 获取list节点
        const listNode = newVDom.children.find(child => child.props.key === 'list');
        // 删除最后一个li节点
        if(listNode.children.length > 0) {
          listNode.children.pop();
        }
        // 获取根元素
        const container = document.querySelector("#root");
        // 使用diff算法对比新旧vDom并更新DOM
        diff(vDom, newVDom, container.parentElement);
        vDom = newVDom;
      }

      function updateItem() {
        // 复制一份vDom
        const newVDom = JSON.parse(JSON.stringify(vDom));
        // 获取list节点
        const listNode = newVDom.children.find(child => child.props.key === 'list');
        // 更新最后一个li节点
        if(listNode.children.length > 0) {
          const lastItem = listNode.children[listNode.children.length - 1];
          lastItem.children = [`项目${listNode.children.length}(已更新)`];
        }
        // 获取根元素
        const container = document.querySelector("#root");
        // 使用diff算法对比新旧vDom并更新DOM
        diff(vDom, newVDom, container.parentElement);
        vDom = newVDom;
      }

      function queryItem() {
        // 获取list节点
        const listNode = vDom.children.find(child => child.props.key === 'list');
        // 显示当前列表项数量
        console.log(`当前列表共有 ${listNode.children.length} 个项目`);
      }

      function initVDom() {
        vDom = createElement(
          "div",
          { id: "root", key: "root" },
          createElement("h1", { key: "title" }, "虚拟DOM演示"),
          createElement(
            "p",
            { style: "color: blue", key: "paragraph" },
            "这是一个段落"
          ),
          createElement(
            "ul",
            { key: "list" },
            createElement("li", { key: "item-1" }, "项目1"),
            createElement("li", { key: "item-2" }, "项目2")
          ),
          createElement(
            "button",
            { onclick: "addItem()", key: "btn-add" },
            "添加项目"
          ),
          createElement(
            "button",
            { onclick: "deleteItem()", key: "btn-delete" },
            "删除项目"
          ),
          createElement(
            "button",
            { onclick: "updateItem()", key: "btn-update" },
            "更新项目"
          ),
          createElement(
            "button",
            { onclick: "queryItem()", key: "btn-query" },
            "查询项目"
          )
        );
      }
      initVDom();

      // 渲染到页面
      const renderView = () => {
        const rootElement = render(vDom);
        document.body.appendChild(rootElement);
      };
      const deleteView = () => {
        const container = document.querySelector("#root");
        document.body.removeChild(container);
      };
      renderView();
    </script>
  </body>
</html>
